#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stddef.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <pthread.h>

#include "memalloc.h"


volatile int allocator_created = 0;
volatile int allocator_initialized = 0;
pthread_mutex_t mutex;

allocator* mem_alloc;

size_t ma_alloc(size_t size) {
    if (allocator_initialized == 0) {
        perror("ma_alloc");
        errno = ECANCELED;
        return 0;
    } 

    /* in case of zero size we automatically set it to minimum - that is 1 byte */
    if (size == 0) size = 1;

    int size_aligned = align_to_pagesize(size);
    if (mem_alloc->mem_size < size_aligned) {
        perror("ma_alloc");
        errno = E2BIG;
        return 0;
    }

    pthread_mutex_lock(&mutex);
    swapped_chunk_borders* swp_chunk_borders = search_free_space_in_swap(align_to_pagesize(size));
    if (swp_chunk_borders == NULL) {
        perror("ma_alloc");
        errno = ENOMEM;
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    if (mem_alloc->max_id >= UINT_MAX) {
        perror("ma_alloc");
        errno = EMLINK;

        /* now we don't need this info about chunk borders */
        free(swp_chunk_borders);

        pthread_mutex_unlock(&mutex);
        return 0;
    }

    size_t id = mem_alloc->max_id;

    if (create_and_register_chunk_in_swap(size, id, swp_chunk_borders) == 0) {
        perror("ma_alloc");

        /* now we don't need this info about chunk borders */
        free(swp_chunk_borders);

        pthread_mutex_unlock(&mutex);
        return 0;
    }

    mem_alloc->max_id++;

    pthread_mutex_unlock(&mutex);

    /* now we don't need this info about chunk borders */
    free(swp_chunk_borders);

    return id;
}


void* ma_get(size_t id) {
    if (allocator_initialized == 0) {
        perror("ma_get");
        errno = ECANCELED;
        return NULL;
    } 

    /* check if id is a valid number */
    if ((id == 0) || (id >= UINT_MAX)) {
        perror("ma_get");
        errno = EINVAL;
        printf("invalid id\n");
        return NULL;
    }

    pthread_mutex_lock(&mutex);
    uploaded_chunk* upd_chunk = search_chunk_with_given_id_in_memory(id);

    if (upd_chunk != NULL) {
        upd_chunk->access_counter++;
        pthread_mutex_unlock(&mutex);
        printf("founded chunk in mem\n");
        return upd_chunk->addr;
    }

    swapped_chunk* swp_chunk = search_chunk_with_given_id_in_swap(id);
    if (swp_chunk == NULL) {
        perror("ma_get");
        errno = EFAULT;
        pthread_mutex_unlock(&mutex);
        printf("haven't found chunk %lu in swap\n", id);
        return NULL;
    }

    uploaded_chunk_borders* upd_chunk_borders = search_free_space_in_buffer(swp_chunk->size_aligned);
    if (upd_chunk_borders == NULL) {
        upd_chunk_borders = find_chunks_to_swap_out(swp_chunk->size_aligned);
        if (upd_chunk_borders == NULL) {
            errno = ENOMEM;
            pthread_mutex_unlock(&mutex);
            printf("problems with chunk borders\n");
            return NULL;
        }

        uploaded_chunk* chunk = upd_chunk_borders->prev;

        /* in order to use this borders to map our chunk, we should redefine it */
        upd_chunk_borders->prev = upd_chunk_borders->prev->prev;

        uploaded_chunk* next_chunk;
        do {
            next_chunk = chunk->next;
            if (swap_out_chunk(chunk) == 0)
                printf("%s\n", strerror(errno));
            chunk = next_chunk;
        } while (chunk != upd_chunk_borders->next->next);

        /* in order to use this borders to map our chunk, we should redefine it */
        upd_chunk_borders->next = chunk;
        pthread_mutex_unlock(&mutex);
    }

    void* addr = swap_in_chunk(swp_chunk, upd_chunk_borders);
    if (addr == NULL) {
        printf("%s\n", strerror(errno));
        pthread_mutex_unlock(&mutex);
        printf("could not swap_in_chunk\n");
        return NULL;
    } 

    pthread_mutex_unlock(&mutex);

    free(upd_chunk_borders);

    //printf("Get SUCCESS\n");
    return addr;
}


int ma_release(size_t id) {
    if (allocator_initialized == 0) {
        perror("ma_release");
        errno = ECANCELED;
        return 0;
    } 

    /* check if id is a valid number */
    if ((id == 0) || (id >= UINT_MAX)) {
        perror("ma_release");
        errno = EINVAL;
        return 0;
    }

    pthread_mutex_lock(&mutex);

    uploaded_chunk* upd_chunk = search_chunk_with_given_id_in_memory(id);
    if ((upd_chunk == NULL) || (upd_chunk->access_counter == 0)) {
        perror("ma_release");
        errno = EFAULT;
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    upd_chunk->access_counter--;
    pthread_mutex_unlock(&mutex);
    return 1;
}

int ma_free(size_t id) {
    if (allocator_initialized == 0) {
        perror("ma_free");
        errno = ECANCELED;
        return 0;
    } 

    /* check if id is a valid number */
    if ((id == 0) || (id >= UINT_MAX)) {
        perror("ma_free");
        errno = EINVAL;
        return 0;
    }

    pthread_mutex_lock(&mutex);

    uploaded_chunk* upd_chunk = search_chunk_with_given_id_in_memory(id);
    
    if ((upd_chunk != NULL) && (upd_chunk->access_counter != 0)) {
        perror("ma_free");
        errno = EFAULT;
        pthread_mutex_unlock(&mutex);
        return 0;
    } 


    if (upd_chunk != NULL) {
        if (msync(upd_chunk->addr, upd_chunk->size_aligned, MS_SYNC) == -1) {
            perror("msync");
            pthread_mutex_unlock(&mutex);
            return 0;
        }

        poison_memory_after_free(upd_chunk->addr, upd_chunk->size_aligned);
    }
   
    
    if (swap_out_chunk(upd_chunk) == 0) {
        printf("%s\n", strerror(errno));
        perror("ma_free");
        errno = EFAULT;
        pthread_mutex_unlock(&mutex);
        return 0;
    }

  

    swapped_chunk* swp_chunk = search_chunk_with_given_id_in_swap(id);
    if (swp_chunk == NULL) {
        perror("ma_free");
        errno = EFAULT;
        pthread_mutex_unlock(&mutex);
        return 0;
    }


    delete_from_swap_file(swp_chunk);
    
    pthread_mutex_unlock(&mutex);

    return 1;
}




int ma_init(size_t mem, off_t swap, const char* swap_path) {
    if (__sync_bool_compare_and_swap(&(allocator_created), 0, 1)) {
        /* if allocator is not created yet, do it */
        size_t aligned_size = align_to_pagesize(mem);
        int fd;
        void* buf;

        int returned_value = posix_memalign(&buf, sysconf(_SC_PAGESIZE), aligned_size);
        /* in case of an error posix_memalign() does not set errno, so we should do it ourself */
        if (returned_value != 0) {
            errno = ENOMEM;
            /* we have to set allocator_created to 0 in order to show other threads which may wait for allocator's initialization that it has failed */
            allocator_created = 0;
            return 0;
        }

        poison_uninitialized_memory(buf, aligned_size);

        /* we give access rights only to user, not for group or others */
        if ((fd = open(swap_path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR)) == -1) {
            perror("open");
            /* we have to set allocator_created to 0 in order to show other threads which may wait for allocator's initialization that it has failed */
            allocator_created = 0;
            return 0;
        }

        returned_value = posix_fallocate(fd, 0, swap);
        /* in case of an error posix_fallocate() does not set errno, so we should do it ourself */
        if (returned_value != 0) {
            perror("posix_fallocate");
            switch(returned_value) {
                case ENOSPC: errno = ENOSPC; break;
                case EFBIG: errno = EFBIG; break;
                default: errno = EFAULT; break;
            }
            /* we don't check return value of file close() */
            close(fd);
            /* we have to set allocator_created to 0 in order to show other threads which may wait for allocator's initialization that it has failed */
            allocator_created = 0;
            /* return 0 in case of an error */
            return 0;
        }

        mem_alloc = (allocator*)malloc(sizeof(allocator));
        if (mem_alloc == NULL) {
            /* we don't check return value of file close() */
            close(fd);
            errno = ENOMEM;
            /* we have to set allocator_created to 0 in order to show other threads which may wait for allocator's initialization that it has failed */
            allocator_created = 0;
            return 0;
        }

        mem_alloc->mem_size = aligned_size;
        mem_alloc->swap_size = swap;
        mem_alloc->buf_begin = buf;
        mem_alloc->buf_end = (void*)((char*)buf + aligned_size - 1);
        mem_alloc->max_id = 1;
        mem_alloc->swap_fd = fd;
        mem_alloc->swapped_chunks_list = NULL;
        mem_alloc->uploaded_chunks_list = NULL;

        pthread_mutex_init(&mutex, NULL);
        allocator_initialized = 1;

    } else { 
        /* allocator is already created, but we should check if it is initialized. If not, we should wait its initialization */
        while (!allocator_initialized) {
            /* 
                check if another thread had failed allocator's initialization, so it finished with error and set allocator_created again to false (0);  
                it means that actually allocator was not created and we should leave 

                otherwise (if we don't check this condition) we risk to wait forever for event that is not going to happen 
            */
            if (allocator_created == 0) {
                /* we cannot know what happened during allocator's initialization, so we through general error */
                errno = EFAULT;
                return 0;
            }
        }
        //printf("Waiting for allocator to be initialized\n");
    }
    /* return 1 on SUCCESS */
    return 1;

}


void ma_deinit() {
    /* we set allocator_initialized to false in order to prevent undefined behaviour of ma_* functions called during\after ma_deinit call */
    if (__sync_bool_compare_and_swap(&(allocator_initialized), 1, 0)) {
        allocator_created = 0;
        /* close swap file */ 
        if (close(mem_alloc->swap_fd) != 0) {
            perror("close");
            return;
        }

        /* free buffer */
        if (mem_alloc->buf_begin != NULL) free(mem_alloc->buf_begin);

        /* free lists */
        uploaded_chunk *upd_chunk_next;
        while (mem_alloc->uploaded_chunks_list != NULL) {
            upd_chunk_next = mem_alloc->uploaded_chunks_list->next;
            free(mem_alloc->uploaded_chunks_list);
            mem_alloc->uploaded_chunks_list = upd_chunk_next;
        }

        swapped_chunk* swp_chunk_next;
        while (mem_alloc->swapped_chunks_list != NULL) {
            swp_chunk_next = mem_alloc->swapped_chunks_list->next;
            free(mem_alloc->swapped_chunks_list);
            mem_alloc->swapped_chunks_list = swp_chunk_next;
        }

        pthread_mutex_destroy(&mutex);
        free(mem_alloc);
    } 

    /* if allocator has been already deinitialized or haven't been created, there is nothing to do */
    return;

}
